package csel.model.entity;

import java.util.EnumMap;
import org.apache.log4j.Logger;

import csel.model.*;
import csel.model.items.*;
import csel.model.skills.NonRangedActiveSkill;
import csel.model.skills.PassiveSkill;
import csel.model.skills.RangedActiveSkill;
import csel.model.skills.Skill;
import csel.model.skills.SkillsHolder;
import csel.model.skills.SkillsProxy;

public abstract class Character extends Entity implements Observer<CharacterStats>
{
	private static final long serialVersionUID = 12L;

	private static final int POINTS_PER_LEVEL = 5;

	private final Inventory<EquippableItem> inventoryEquippable;
	private final Inventory<UseableItem> inventoryUseable;

	private final EquippedItems equippedItems;
	private volatile Vehicle vehicleIn;
	private volatile VehicleState vehicleState;

	private SkillsHolder mySkills;

	private Occupation occupation;
	private final CharacterStatsHolder stats;

	private int flexBucks;
	private int freeSkillPoints;

	private static final Logger logger = Logger.getLogger("csel.model");

	Character(Occupation occupation, String name)
	{
		super(occupation.toString(), name);

		inventoryEquippable = occupation.getStartingInventoryEquippable();
		inventoryUseable = occupation.getStartingInventoryUseable();
		equippedItems = new EquippedItems();
		mySkills = new SkillsHolder();

		this.occupation = occupation;

		//set initial stats
		flexBucks = 0;
		freeSkillPoints = 0;
		stats = occupation.getInitialStats();
		occupation.setInitialResistances(stats);
		
		vehicleState = new PedState();
	}
	public void init()
	{
		//HACK, MUST BE CALLED AFTER CONSTRUCTING ANY CHARACTER
		stats.addObserver(this);

		occupation.accept(new MyOccupationSkillVisitor(), stats.getStat(CharacterStats.Level), this);
	}
	public Occupation getOccupation()
	{
		return(occupation);
	}

	public int getFlexBucks()
	{
		return flexBucks;
	}

	public void setFlexBucks(int flexBucks)
	{
		this.flexBucks = flexBucks;

		notifyO();
	}

	public int getFreeSkillPoints()
	{
		return freeSkillPoints;
	}

	public void giveSkillPoints(int num)
	{
		freeSkillPoints += num;
	}


	public SkillsProxy getMySkills()
	{
		return mySkills;
	}

	public void useSkillOnSelf(int slot) throws SkillIndexOutOfBoundsException
	{
		try
		{
			mySkills.getNonRangedSkills().get(slot).use(this);
		} catch (IndexOutOfBoundsException e)
		{
			throw new SkillIndexOutOfBoundsException();
		}
	}

	public void useSkillOnSelf(NonRangedActiveSkill skill)
	{
		skill.use(this);
	}

	public void useSkillInDirectionFacing(int slot)
			throws SkillIndexOutOfBoundsException
	{
		try
		{
			mySkills.getActiveSkills().get(slot).use(getDirection(),
					getTile().getNeighbor(getDirection()));
		} catch (IndexOutOfBoundsException e)
		{
			throw new SkillIndexOutOfBoundsException();
		}
	}
	


	public void useSkillInDirectionFacing(RangedActiveSkill skill)
	{
		skill.use(getDirection(),
				getTile().getNeighbor(getDirection()));
		
	}

	public void upgradeSkill(int slot) throws SkillIndexOutOfBoundsException
	{
		try
		{
			if (freeSkillPoints > 0)
			{
				mySkills.getAllSkills().get(slot).allocatePoints(1);
				freeSkillPoints--;
				notifyO();
			}
		} catch (IndexOutOfBoundsException e)
		{
			throw new SkillIndexOutOfBoundsException();
		}
	}

	public void upgradeSkill(Skill skill)
	{
		if (freeSkillPoints > 0)
		{
			skill.allocatePoints(1);
			freeSkillPoints--;
			notifyO();
		}
	}
	
	/* ITEM MANAGEMENT */

	public ItemCollection<EquippableItem> getEquippableItems()
	{
		return inventoryEquippable;   // ItemCollection is immutable
	}

	public ItemCollection<UseableItem> getUsableItems()
	{
		return inventoryUseable;   // ItemCollection is immutable
	}

	public void pickUp(EquippableItem ei)
	{
		inventoryEquippable.add(ei);

		notifyO();
	}

	public void pickUp(UseableItem ui)
	{
		inventoryUseable.add(ui);

		notifyO();
	}

	public void equip(EquippableItem ei)
	{
		if (ei == null)
			throw new IllegalArgumentException("null parameter");

		if(inventoryEquippable.remove(ei))
		{
			EquippableItem current = equippedItems.remove(ei.getGoesOn());
			if(current != null)
			{
				inventoryEquippable.add(current);
			}
			equippedItems.add(ei);

			notifyO();
		}
	}

	public void equip(int slot) throws ItemIndexOutOfBoundsException
	{
		equip(inventoryEquippable.getItem(slot));
	}

	public EquippedItems getEquipment()
	{
		//TODO fix me
		/*
		 * how???? Do you want me to make you return a list of equippableItems?
		 * Eh... maybe later...
		 */
		return equippedItems;
	}

	public void unequip(EquippableItem ei)
	{
		if (ei == null)
			throw new IllegalArgumentException("null parameter");

		EquippableItem current = equippedItems.remove(ei.getGoesOn());
		if(current != null)
		{
			inventoryEquippable.add(current);

			notifyO();
		}
	}

	public void unequip(BodyParts bp)
	{
		EquippableItem wasEquipped = equippedItems.remove(bp);
		if(wasEquipped != null)
		{
			inventoryEquippable.add(wasEquipped);
			notifyO();
		}
	}

	public void drop(UseableItem ui)
	{
		if (ui == null)
			throw new IllegalArgumentException("null parameter");

		if (getTile().getItem() == null)
		{
			if(inventoryUseable.remove(ui))
			{
				getTile().setItem(ui);
			}

			notifyO();
		}
		else
		{
			logger.warn("Failed to drop UsableItem on full tile.");
		}
	}

	public void drop(EquippableItem ei)
	{
		if (ei == null)
			throw new IllegalArgumentException("null parameter");

		if(getTile().getItem() == null)
		{
			if(inventoryEquippable.remove(ei))
			{
				getTile().setItem(ei);
			}

			notifyO();
		}
		else
		{
			logger.warn("Failed to drop EquippableItem on full tile.");
		}
	}

	public int getCharacterStat(CharacterStats cs)
	{
		if(cs==CharacterStats.Lives)
			return stats.getStat(cs);
		return (calculateWithBonus(cs));
	}
	private int calculateWithBonus(CharacterStats cs)
	{
		int base = stats.getStat(cs);
		EnumMap<BodyParts, EquippableItem> allEquipped =  equippedItems.getAllEquippedItems();
		for(BodyParts bp: allEquipped.keySet())
		{
			EquippableItem ei = allEquipped.get(bp);
			if(ei!=null)
			{
				base = ei.applyBonus(cs, base);
			}
		}
		if(cs == CharacterStats.Offense)
		{
			Weapon w = equippedItems.getWeapon();
			for(PassiveSkill ps: mySkills.getPassiveSkills())
			{
				base = ps.applyOffensiveBonus(w, base);
			}
		}
		if(cs == CharacterStats.Bargaining){
			base = 0;
			for(PassiveSkill ps: mySkills.getPassiveSkills())
			{
				base = ps.applyBonus(CharacterStats.Bargaining, base);
			}
		}
		return(base);
	}
	public CharacterStatsHolder getCharacterStatsHolder()
	{
		return stats;
	}
	public void setCharacterStat(CharacterStats cs, int value)
	{
		stats.setStat(cs, value);
	}

	public abstract String talk(Character c);

	public boolean canTrade(Character c)
	{
		return (false);
	}

	public void giveTo(Character c, UseableItem ui)
	{
		c.takeFrom(this, ui);
		inventoryUseable.remove(ui);

		notifyO();
	}

	public void giveTo(Character c, EquippableItem ei)
	{
		c.takeFrom(this, ei);
		inventoryEquippable.remove(ei);

		notifyO();
	}

	public void takeFrom(Character c, UseableItem ui)
	{
		inventoryUseable.add(ui);

		notifyO();
	}

	public void takeFrom(Character c, EquippableItem ei)
	{
		inventoryEquippable.add(ei);

		notifyO();
	}

	public void sellTo(Character c, UseableItem ui) throws NotEnoughMoneyException
	{
		int price = c.getSellPrice(this, ui);

		if(c.getFlexBucks()<price)
			throw new NotEnoughMoneyException(String.format("Buyer does not have %d flexBucks.", price));

		if(price != -1)
		{
			flexBucks += price;
			c.setFlexBucks(c.getFlexBucks()-price);
			inventoryUseable.remove(ui);
			c.inventoryUseable.add(ui);

			notifyO();
		}
		else
		{
			//TODO how do we handle not wanting to trade
		}
	}

	public void sellTo(Character c, EquippableItem ei) throws NotEnoughMoneyException
	{
		int price = c.getSellPrice(this, ei);

		if(c.getFlexBucks()<price)
		{
			throw new NotEnoughMoneyException(String.format("Buyer does not have %d flexBucks.", price));
		}

		if(price != -1)
		{
			flexBucks += price;
			c.setFlexBucks(c.getFlexBucks()-price);
			inventoryEquippable.remove(ei);
			c.inventoryEquippable.add(ei);

			notifyO();
		}
		else
		{
			//TODO how do we handle not wanting to trade
		}
	}

	public void buyFrom(Character c, UseableItem ui) throws NotEnoughMoneyException
	{
		int price = c.getBuyPrice(this, ui);

		if(price > flexBucks)
		{
			throw new NotEnoughMoneyException(String.format("You do not have %d flexBucks.", price));
		}

		if(price != -1)
		{
			flexBucks -= price;
			c.setFlexBucks(c.getFlexBucks()+price);
			inventoryUseable.add(ui);
			c.inventoryUseable.remove(ui);

			notifyO();
		}
		else
		{
			//TODO how do we handle not wanting to trade
		}
	}

	public void buyFrom(Character c, EquippableItem ei) throws NotEnoughMoneyException
	{
		int price = c.getBuyPrice(this, ei);

		if(price>flexBucks)
		{
			throw new NotEnoughMoneyException(String.format("You do not have %d flexBucks.", price));
		}

		if(price != -1)
		{
			flexBucks -= price;
			c.setFlexBucks(c.getFlexBucks()+price);
			inventoryEquippable.add(ei);
			c.inventoryEquippable.remove(ei);

			notifyO();
		}
		else
		{
			//TODO how do we handle not wanting to trade
		}
	}

	/**
	 * 
	 * @return the price for which this Character is willing to buy the item
	 */
	public abstract int getBuyPrice(Character c, TakeableItem ti);

	/**
	 * 
	 * @return the price for which this Character is willing to sell the item
	 */
	public abstract int getSellPrice(Character c, TakeableItem ti);
	
	/* MOVEMENT */

	@Override
	public boolean isImpassable()
	{
		return true;
	}

	@Override
	protected int getSpeed()
	{
		return getCharacterStat(CharacterStats.Speed);
	}
	
	public synchronized boolean move(Tile dest)
	{
		return vehicleState.move(dest);
	}
	
	public synchronized void move(Direction dir)
	{
		vehicleState.move(dir);
	}
	
	public Tile getTile()
	{
		return vehicleState.getTile();
	}
	
	public Location getLocation()
	{
		return vehicleState.getLocation();
	}
	
	public void stop()
	{
		vehicleState.stop();
	}
	
	/* VEHICLE STUFF */
	
	/* FIXME */
	
	protected synchronized void getIn(Vehicle v)
	{
		if (!v.enter(this))
		{
			logger.info(String.format("%s could not enter vehicle!", this));
			return;
		}
		
		stop();
		vehicleIn = v;
		vehicleState = new DrivingState();
		
		logger.info("Vehicle entered!");
	}
	
	public synchronized void getOut()
	{
		if (vehicleIn == null)
			throw new IllegalStateException("Not in a Vehicle, silly!");

		if (vehicleIn.canLeave(this))
		{
			vehicleState = new PedState();
			vehicleIn.leave(this);
		}
	}
	
	public Vehicle getVehicle()
	{
		return vehicleIn;
	}

	/**
	 * State pattern for driving vehicles.
	 */
	protected interface VehicleState extends java.io.Serializable
	{
		Tile getTile();
		Location getLocation();
		
		boolean move(Tile dest);
		void move(Direction dir);
		void stop();
	}
	
	protected class PedState implements VehicleState
	{
		private static final long serialVersionUID = 5L;	// should never change
		
		public boolean move(Tile dest)
		{
			return Character.super.move(dest);
		}
		
		public Tile getTile()
		{
			return Character.super.getTile();
		}
		
		public Location getLocation()
		{
			return Character.super.getLocation();
		}
		
		public void move(Direction dir)
		{
			Character.super.move(dir);
		}
		
		public void stop()
		{
			Character.super.stop();
		}
	}
	
	protected class DrivingState implements VehicleState
	{
		private static final long serialVersionUID = 5L;	// should never change
		
		public boolean move(Tile dest)
		{
			return vehicleIn.move(dest);
		}
		
		public Tile getTile()
		{
			return vehicleIn.getTile();
		}
		
		public Location getLocation()
		{
			return vehicleIn.getLocation();
		}
		
		public void move(Direction dir)
		{
			vehicleIn.move(dir);
		}
		
		public void stop()
		{
			vehicleIn.stop();
		}
	}
	
	/* OTHER */

	@Override
	public void damage(DamageTypes dT, int dam)
	{
		double resistanceModifier = (stats.getResistance(dT)/100.0);
		int damage = (int)(dam - dam * resistanceModifier);
		stats.setStat(CharacterStats.Damage, stats.getStat(CharacterStats.Damage)+damage);
		System.out.println(this.getClass().getSimpleName()+ " Damage = "+ stats.getStat(CharacterStats.Damage));
		System.out.println(this.getClass().getSimpleName()+ " Current Life = " + stats.getStat(CharacterStats.CurrentLife));
		System.out.println(this.getClass().getSimpleName()+ " Max Life = " + stats.getStat(CharacterStats.MaxLife));
	}

	@Override
	public boolean canEnter(MobileObject obj, BlockNotifier notifier)
	{
		if (notifier != null)
			notifier.blockedBy(this);

		return false;
	}

	public void attack()
	{
		Weapon w = equippedItems.getWeapon();
		Entity eToAttack = getTile().getNeighbor(getDirection()).getEntity();
		if(eToAttack!=null)
		{
			if(w!=null)
			{
				w.attackWith(this, getTile().getNeighbor(getDirection()).getEntity());
			}
			else
			{
				eToAttack.damage(DamageTypes.Direct, getCharacterStat(CharacterStats.Offense));
			}
			eToAttack.pissTaitOff(this);
		}
	}
	public void useItem(int slot) throws ItemIndexOutOfBoundsException
	{
		UseableItem ui = inventoryUseable.getItem(slot);
		if(!ui.use(this))
		{
			inventoryUseable.remove(ui);
		}
	}
	public void gimpThrowItem(int slot) throws ItemIndexOutOfBoundsException
	{
		Tile facingTile = getTile().getNeighbor(getDirection());
		
			UseableItem ui = inventoryUseable.getItem(slot);

			if(facingTile.getEntity()!=null)
			{
				inventoryUseable.remove(ui);
				ui.use(facingTile.getEntity());
			}
			else if(facingTile.getItem()==null&&(facingTile.canEnter(this, null)))
			{
				inventoryUseable.remove(ui);
				facingTile.setItem(ui);
			}
		
	}
	public void dropUseableItem(int slot)
	{
		drop(inventoryUseable.getItem(slot));
	}
	public void dropEquippableItem(int slot)
	{
		drop(inventoryEquippable.getItem(slot));
	}
	public UseableItem stealFrom()
	{
		int itemToSteal = (int)(Math.random() * 123456);
		UseableItem stealing = inventoryUseable.getItem(itemToSteal%inventoryUseable.size());
		inventoryUseable.remove(stealing);
		return(stealing);
	}
	public void addToInventoryUseable(UseableItem ui)
	{
		inventoryUseable.add(ui);
	}

	public String toString()
	{
		return String.format("%s (%s)", getName(), occupation);
	}
	private void levelUpUpdate()
	{
		levelUpAction();
	}
	@Override
	public void levelUp()
	{
		stats.levelUp();
	}

	public void levelUpAction()
	{
		giveSkillPoints(POINTS_PER_LEVEL);

		occupation.accept(new MyOccupationSkillVisitor(), stats.getStat(CharacterStats.Level), this);

		notifyO();
	}

	public abstract void dieAction();
	
	public void mupdate(Object o, CharacterStats cs)
	{
		switch(cs)
		{
		case Level:
			levelUpUpdate();
			break;
		case Lives:
			dieAction();
			break;
		}
	}

	private class MyOccupationSkillVisitor implements OccupationSkillGetterVisitor
	{
		public void visitNonRangedSkill(NonRangedActiveSkill skill)
		{
			mySkills.addSkill(skill);
		}

		public void visitPassiveSkill(PassiveSkill skill)
		{
			logger.info(String.format("%s got a skill: %s!", Character.this, skill));
			mySkills.addSkill(skill);
		}

		public void visitRangedSkill(RangedActiveSkill skill)
		{
			mySkills.addSkill(skill);
		}
	}
	
	public boolean can(MoveAbilities ability)
	{
		switch (ability)
		{
		case Swim:
			return true;
		default:
			return super.can(ability);
		}
	}
}
